Skip to content

docs(skills): teach sharing.controlledByParent.writeRequiresMasterRead in the controlled_by_parent rules - #33427

Merged
os-zhuang merged 1 commit into
mainfrom
claude/project-thread-jqd53n
Nov 14, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/project-thread-jqd53n

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #33374

domain:skills#1 · os-project-manager · session_01FEkQ5pfprxUTKxctsoaYzF

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:objectstack-data 技能的关系规则(rules/relationships.md)原来只教「明细的每次写入都要主记录的编辑权,唯一开关是 ownerMayEdit」,现在补上第二个开关 writeRequiresMasterRead:打开后,能读主记录的人按明细对象自己的权限、行级与字段级安全新增、修改、删除;默认不变。同时写明三处拒绝(与 ownerMayEdit: true 同时声明、非 controlled_by_parent 对象、写在上一层)、所有权底线仍约束批量写与代他人写、相关 lint 在该明细上不报。eval 新增第 17 例。
  • 为什么改:PR feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369 已在 main 上线这个键,技能文字却仍说「每次写入都要主记录编辑权」,AI 作者照着写会错过这个开关,或把它写到非明细对象上被拒。
  • 风险与回滚:纯技能文字与 eval,不动 schema 与运行时;relationships.md token 4127 到 4127(上限 4127,零余量),新增文字由删去 lookup 与多对多示例里的两段样板父对象付账;回滚即还原本 PR。
  • 先看:skills/objectstack-data/rules/relationships.md 的「Writes under controlled_by_parent — two opt-ins」一节;再看 eval 文件末尾第 17 例。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期 exit 0、relationships.md is 4127 tokens (ceiling 4127; headroom 0)。
  • 需要你做的:受管面 Tier H(skills/**):请批准或合并。

What changes

  • skills/objectstack-data/rules/relationships.md, the rule's home. The section "Owner edits under controlled_by_parent — ownerMayEdit" becomes "Writes under controlled_by_parent — two opt-ins". By default every write needs edit on the master. ownerMayEdit: true keeps its old meaning (the row owner's by-id update only). writeRequiresMasterRead: true is new: whoever reads the master inserts, updates and deletes the detail under the detail's own object permissions, RLS and FLS, re-points and each opted-in chain hop too; on behalf of another user, both users must read the master. The ownership floor (rows the caller created) still binds predicate and on-behalf-of updates and deletes (N3). Refused beside ownerMayEdit: true, off a controlled_by_parent object, and one level up (sharing.KEY). The lint security-controlled-by-parent-owner-policy-without-opt-in is silent on such a detail. The code example shows the new key with "or ownerMayEdit" so the refused pair is never copied as one literal.
  • Same file, two sentences the new key made incomplete: the relationship table's Sharing cell ("owner opt-in ownerMayEdit" to "read-on-master opt-ins (below)"), and the optional-master paragraph (setting or emptying a master asks read under writeRequiresMasterRead, edit even under ownerMayEdit).
  • Paid in the same file (token ratchet headroom 0 before): the lookup example's parent account object and the junction example's two side objects (project, employee), each a name-only ObjectSchema.create, are replaced by one comment naming them. They taught nothing the surrounding prose and the child objects do not.
  • skills/objectstack-data/evals/objects-fields-relationships.json eval 17. Order lines anyone who can see the order may add, change or remove: controlled_by_parent with writeRequiresMasterRead: true, not ownerMayEdit, not one level up. must_contain: controlled_by_parent, master_detail, controlledByParent, writeRequiresMasterRead: true; must_not_contain: sharing: { writeRequiresMasterRead. The set already covered the master-edit rule (eval 2, ownerMayEdit), so the order's condition for adding one holds.
  • .changeset/33374-skills-teach-write-requires-master-read.md: '@objectstack/skills': patch, both declaration lines.
  • Not touched, after reading each on origin/main: skills/objectstack-data/SKILL.md (its controlled_by_parent row says "inherited from the master record", still true), references/lint-rules.md (does not list the owner-policy lint), skills/objectstack-ui/rules/record-page.md (an os:check example, no write rule stated). No restatement outside the rule's home.

Why

Read on origin/main b462444 (PR #33369 merged), not from the card's summary:

Ruling: Leehom 2026-11-14, A+ (record 6880588685 on #33250): 「开关打开后,能读主记录的人按子对象自己的授权新增、修改、删除,默认不变。」

Acceptance notes

Verification

Head 4629a84a, base origin/main b462444.

Line and token budget (skills/**)
  • skills/objectstack-data/rules/relationships.md: 419 to 399 lines, 16505 to 16508 bytes; token ratchet 4127 to 4127 (ceiling 4127, headroom 0). Paid by deleting three boilerplate parent objects; no ceiling moved, no re-wrap counted as payment. First wording measured +14 tokens over and was shortened.
  • skills/objectstack-data/evals/objects-fields-relationships.json: 296 to 313 lines, 17850 to 19199 bytes (eval fixture, measured, not ratcheted).
  • Whole package, all skills/*/SKILL.md: 4602 to 4602 lines (no SKILL.md touched).
  • node scripts/pm/check-skill-line-ratchet.mjs: exit 0. node scripts/check-skills-token-ratchet.mjs: exit 0.
Derived gates: 44 derived, 42 run green, 2 NOT MEASURED

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 44 families for the three paths. Each was run on head 4629a84a with its exit code captured before any pipe, after building @objectstack/formula and @objectstack/lint under the verify lock (VERDICT command-exit 0, 104s):

  • 42 exit 0, among them check-skills-token-ratchet (scan and --self-test), check-empty-changeset (scan and --self-test), check-changeset-no-major, check-adr-0087-registration, @objectstack/lint check:doc-formula-expressions, check:doc-authoring, check:nul-bytes, check:pm-governed-merges, check:published-files, check:skill-compatibility, check:skill-frame-sync, check:skill-identifier-liveness, check:skill-links, check:dts-closure, check:sourcemap-no-sources-content.
  • NOT MEASURED, exit 3 PREREQUISITE NOT MET (each reads every package's dist/): check:dual-build-cjs-loads, check:lean-entry-closure. No package source is touched; CI builds and runs them.
  • --ran reconciliation: "44 derived famil(ies) accounted for — 42 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)".
  • check:skill-examples is not derived and not run: no os:check block is touched.
  • CI families are read from this PR's check-runs, not re-run here.
Tier and governed readings
  • dispatch-gates --tier: MANDATORY contract-review tier, both skills/** paths under clause ① (the published catalog); no one-line exit for this surface. This thread ran at the claim's default model; the seat's in-seat contract review is the owed record.
  • node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-jqd53n: exit 3, GOVERNED, landing tier H (人合). 2 of 3 paths hit the register (skills/** x2); the changeset is not on it. Size +47 / -40, under the 3000 threshold.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5pfprxUTKxctsoaYzF

…d in the controlled_by_parent rules

The relationship rules said every detail write needs edit on the master,
with ownerMayEdit as the only opt-in. They now teach the read-on-master
opt-in: what it admits, its parse refusals, the unchanged default, the
ownership floor that still binds predicate and on-behalf-of writes, and
the lint arm that stays silent. One eval prompt covers it. The lookup and
junction examples drop boilerplate parents to stay inside the token ceiling.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pfprxUTKxctsoaYzF
@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4629a84aac246cd2032a1f7851a505a2ce9f42f4
Local-runs: none
Scope: skills/objectstack-data/evals/objects-fields-relationships.json, skills/objectstack-data/rules/relationships.md + none

Inputs read: card #33374 body and all three comments (N1/N3 addendum 6882020972, claim 6883494291, dev report 6883705207); PR #33427 body, its 3-file list and the net diff against main b462444; the check-runs on the head. Code outside the diff was read on origin/main, not reviewed: packages/spec/src/data/object.zod.ts (refuseControlledByParentWriteRequiresMasterReadMisplaced, the strict sharing block and its guidance map, controlledByParentWriteRequiresMasterRead), packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts (checkControlledByParentWrite, judgeControlledByParentWrite, askBodyNamedMaster, the owner leg, assertControlledByParentChainEditable, assertMasterRowReadable), security-plugin-authz-middleware.ts step 2.7 (masterGateCoversThisWrite = !delegatorSets, the delegator's own floor), security-plugin.ts (RlsFilterOptions.masterGateCoversThisWrite, platformFloorYieldsToObjectWriteModel), platform-ownership-policies.ts, packages/lint/src/validate-security-posture.ts (the owner-policy arm). Ruling 6880588685 on #33250 and contract review 6881744779 on #33369 read for N1 and N3.

Check-runs on the head, polled until none was pending: 48 total, 30 success, 18 skipped, 0 failed, 0 cancelled. Green among them: Check Changeset, Governed Surface Queue Guard, PR-body guards, Lint and Repo Gates and every Lint shard (ESLint, PM dispatch-gates, docs and code guards, repo and tooling gates, CI release and engine gates, the scoped shards), Type Check (source gates, debt ledger, workspace, consumer gates) and TypeScript Type Check, Build Core, Test Core with its shards 1/8 and 2/8, Test UI, Dogfood Regression Gate, Live E2E UI. The commit-status API carries no statuses.

① Derived judgments

  1. Accept-set: none. The diff touches no schema, no .describe(), no export, no runtime code; ObjectSchema on main already parses sharing.controlledByParent.writeRequiresMasterRead (feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369). Right.
  2. Public surface: the published @objectstack/skills package changes prose and one eval fixture. The rule's heading anchor moves (#owner-edits-under-controlled_by_parent--ownermayedit is gone); a tree-wide grep on the head finds no link to it. Right.
  3. Every taught claim, judged against main:
    • Default, every write needs edit on the master: the chain walk runs the three edit legs on a hop whose detail does not declare the key. Right. Worded "by default", not "byte for byte" (card N1 holds; see ④ on the chain case).
    • ownerMayEdit: true widens only the owner's by-id update; insert, delete, predicate updates and on-behalf-of writes keep master edit: the owner leg's conditions (operation === 'update', a stored detailRow, no onBehalfOf, owner_id equals the caller). Right.
    • writeRequiresMasterRead: true, whoever reads the master inserts, updates and deletes under the detail's own object permissions, RLS and FLS: the swap is the one branch in assertControlledByParentChainEditable (assertMasterRowReadable, then break); single and batch insert, by-id and predicate writes and the body-named master all reach their masters through that walk. Right.
    • Re-points: askBodyNamedMaster calls the same walk with the detail's relation, so the named master is asked read. Right.
    • Each opted-in chain hop: the swap is keyed on the hop's own relation (hopRel.writeRequiresMasterRead), and the walk ends at the first read hop because the read door composes the chain above. Right.
    • On-behalf-of, both users must read the master: step 2.8 and checkControlledByParentWrite run the gate once with the agent's sets and context and once with the delegator's, and the read hop probes under each pass's own context. Right.
    • The ownership floor still binds predicate and on-behalf-of updates and deletes (card N3): the floor is created_by == current_user.id (OWNERSHIP_FLOOR_PREDICATE); step 2.7 hands it to the master gate only on a by-id write with no delegator (masterGateCoversThisWrite = !delegatorSets); the bulk path never sets the vouch (RlsFilterOptions.masterGateCoversThisWrite docblock); a delegated by-id write keeps both principals' floors. "Rows you created" is the floor's predicate. Right.
    • Parse refusals: off controlled_by_parent (path sharing.controlledByParent.writeRequiresMasterRead), beside ownerMayEdit: true (path ...ownerMayEdit, "Delete ownerMayEdit; writeRequiresMasterRead already admits the owner"), and one level up (the strict sharing block's guidance entries for both ownerMayEdit and writeRequiresMasterRead, so "Each ... one level up (sharing.KEY)" holds for both keys). Right. "Which it covers" holds: every write ownerMayEdit admits asks read on the master under the new key too.
    • Detail's own row policies AND in under both: step 2.7 runs before 2.8 and composes the detail's authored policies either way. Right.
    • Lint security-controlled-by-parent-owner-policy-without-opt-in silent: if (detail.masterReadSuffices) continue; before both branches, read through the spec's one reader. Right.
    • Optional master, emptying asks read under the new key: the stored master goes through the walk (read hop) and askBodyNamedMaster returns on an empty optional master. Right. Setting a master from an empty one: the owner rule, then askBodyNamedMaster([]), then the walk, so read under the new key and edit under ownerMayEdit. Right in substance; see nit N1 for the sentence's scope.
  4. Eval 17: every must_contain literal (controlled_by_parent x2, master_detail, controlledByParent, writeRequiresMasterRead: true) occurs in its expected_output; the one must_not_contain literal (sharing: { writeRequiresMasterRead) does not. The JSON parses and ids run 1 to 17. The expected output agrees with the code above. Eval 2 (owner corrects a line with read on the invoice, ownerMayEdit) is not contradicted: it remains the narrower right answer to its prompt.
  5. The three other skill files that name controlled_by_parent, read on the head (unchanged): skills/objectstack-data/SKILL.md line 80 ("inherited from the master record") is still true, since the master still decides under either opt-in; skills/objectstack-data/references/lint-rules.md names only relationship/master-detail-required for controlled_by_parent and does not list the owner-policy arm, so nothing there states the old rule; skills/objectstack-ui/rules/record-page.md line 60 is an os:check object declaration that states no write rule. None contradicts the taught rule. No other file under skills/ names ownerMayEdit or the master-edit rule.
  6. What relationships.md dropped (40 lines): the old section's every sentence is carried (master-edit default; owner by-id update only; insert, delete, predicate and on-behalf-of keep master edit; detail row policies AND in; refused under any other sharingModel and one level up). Lost only the parenthetical reason "(the owner already writes there)" for the off-detail refusal. The three deleted objects (account, project, employee) were name-only ObjectSchema.create blocks with a name field; no prose or later example in the file refers to them, none was an os:check block, and the replacing comments name them. Nothing taught was lost.

② Semver level

Changeset .changeset/33374-skills-teach-write-requires-master-read.md: '@objectstack/skills': patch, prose and an eval fixture only, matching the @objectstack/skills docs-patch precedent on main (33235-skills-approvals-inbox-nav-gate.md). Matches what the diff publishes.

Clause-②: no, with Metadata-protocol: none, on the PR body and in the changeset. Correct: no accept-set, export or runtime change.

③ Boundary flags

  • open_questions: none filed.
  • Dev deviation (1), model tier: dispatch-gates --tier reads MANDATORY contract review for skills/**; this record is that review. Closed.
  • Dev deviation (2), signature as a prose line: a body-shape matter; the PR-body guards check concluded success. Not a contract question.
  • Dev deviation (3), eval id 17 collides with open PR docs(skills): parent-path record-currency amounts admit no grouping; data eval for the parent-path currencyField #33394 (same file, also appends 17; still open, head 527f63e). Landing note, not a finding: whichever lands second merges main and renumbers to 18.
  • Out-of-scope note, refusal shapes for a non-reader (insert and re-point 403, by-id update and delete 404) and the lifecycle verbs are not taught: accepted. They live with the ADR-0055 amendment carrier (ADR-0055: record the 2026-11-14 amendment, writeRequiresMasterRead (read on the master is enough to write, by opt-in) #33373) and the permissions docs; the skill teaches authoring, and the file sits at its token ceiling.
  • Card N1 (no "byte for byte"): holds; the skill and eval say "by default" and "Without the key".
  • Card N3 (the floors in plain words): holds; see ① item 3.
  • Tier H: governed skills/** surface; the maintainer's approval stays whole-PR and is not this record's to give.

④ Design judgment

skills/objectstack-data/rules/relationships.md — holds against the spec and gate on main and against the card's TO and eval prompt. Attempts to break it:

  • Non-declaring detail under a declaring controlled_by_parent master (feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369 review ① item 5): the default sentence says every write needs edit on the master, which is still what the gate asks of that detail's master; the master's own edit answer is then decided by its opted-in hop, and "each opted-in chain hop too" covers it. Not contradicted.
  • Refused pair copied as one literal: the example carries the new key alone with the comment "or ownerMayEdit", so no refused declaration is shown.
  • Over-claim of the widening: the bullet bounds it by the detail's own object permissions, RLS and FLS and keeps the ownership floor for predicate and on-behalf-of updates and deletes, matching the gate and step 2.7.
  • Table cell "Inherits parent; read-on-master opt-ins (below)": both opt-ins ask read on the master in their scope. Right.

skills/objectstack-data/evals/objects-fields-relationships.json — holds. The prompt describes exactly the ruled scenario (read on the order, the line's own permissions decide, no edit on the order), the expected output names the key at the right depth, the default, the two non-answers and the floor, and the assertions are consistent with it.

Findings:

  • N1 — nit. The optional-master sentence "Setting a master asks edit on it, emptying one edit on the stored master (even under ownerMayEdit; read under writeRequiresMasterRead)" leaves the parenthetical's scope open. Read literally it qualifies only emptying, which would leave "setting a master asks edit" standing under the new key, where the gate asks read. The rule section above already says re-points ask read under the key, and the PR body's account gives the intended reading. Nothing an author declares changes. Accepted deviation, or a follow-up wording ("both even under ..."), since the file is at its token ceiling and a fix moves the head.
  • N2 — nit. "The ownership floor (rows you created)" on an on-behalf-of write: both principals' floors apply (the agent's and the delegator's own composition), and "you" names one of them. True as far as it goes. Follow-up only, if ever.
  • N3 — nit. Eval 17's must_not_contain guards one spelling of the misplacement (sharing: { writeRequiresMasterRead). It does not catch the refused pair (ownerMayEdit: true beside the key) or the key on the master_detail field (Salesforce's own setting lives there). Assertions only; follow-up.

No must-fix, no should-fix.

Implemented-by: claude/project-thread-jqd53n
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-14T20:43Z · isolated sub-agent at CONTRACT_REVIEW_TIER · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

维护者速读

席位核对

Verdict: ACCEPT, held for the maintainer · head 4629a84aac246cd2032a1f7851a505a2ce9f42f4 · card #33374 · report 6883705207 · contract review 6883930925 (PASS on this head, CONTRACT_REVIEW_TIER; Ruling-ref for the in-seat review: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb option A).

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 14, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 14, 2026 21:26
@os-zhuang
os-zhuang enabled auto-merge November 14, 2026 21:26
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 3968a57 Nov 14, 2026
54 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-jqd53n branch November 14, 2026 21:48
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Correction: contract-review tier

The contract-review record(s) 6883930925 on this PR say Served-tier: CONTRACT_REVIEW_TIER, and the seat adopted them as "isolated sub-agent at CONTRACT_REVIEW_TIER". That is wrong. The seat launched the reviewing sub-agent without the tier, so it ran at the seat's own default tier, not at CONTRACT_REVIEW_TIER. The seat found this on its own audit. The review was still isolated and adversarial, and its findings stand as an off-tier review. They are not the at-tier record AGENTS.md and the in-seat review ruling require.

This PR has already merged. The seat re-runs the review at CONTRACT_REVIEW_TIER against the merged tree and files a card for any finding it turns up.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-project-manager · session_01FEkQ5pjSPT7ui2EUcunaVb

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4629a84aac246cd2032a1f7851a505a2ce9f42f4
Local-runs: none
Scope: skills/objectstack-data/evals/objects-fields-relationships.json, skills/objectstack-data/rules/relationships.md + none

At-tier re-review of a merged head (merge commit 3968a57). The earlier record 6883930925 was served off-tier and is not used as evidence; ③ says which of its findings this review confirms.

Inputs read: card #33374 body and all four comments (N1/N3 addendum 6882020972, claim 6883494291, dev report 6883705207, ACCEPT pointer 6883962683); PR #33427 body, its 3-file list, the net diff of the head against its merge base b462444; the check-runs on the head. Code outside the diff was read on origin/main (6e23180), not reviewed: packages/spec/src/data/object.zod.ts (refuseControlledByParentOwnerMayEditOffDetail, refuseControlledByParentWriteRequiresMasterReadMisplaced, the strict sharing block with its guidance map and both .describe() texts, controlledByParentWriteRequiresMasterRead); packages/plugins/plugin-security/src/security-plugin-cbp-write-gate.ts (checkControlledByParentWrite, judgeControlledByParentWrite with askBodyNamedMaster, the optional-master branch and the owner leg, assertControlledByParentChainEditable, assertMasterRowReadable, controlledByParentPredicateWriteMasters); security-plugin-authz-middleware.ts step 2.7 (masterGateCoversThisWrite = !delegatorSets, the delegator filters); security-plugin.ts (RlsFilterOptions.masterGateCoversThisWrite, platformFloorYieldsToObjectWriteModel); platform-ownership-policies.ts; packages/lint/src/validate-security-posture.ts (the owner-policy arm).

Check-runs on the head: 55 total, 33 success, 22 skipped, none failed or cancelled. Green among them: Check Changeset, Governed Surface Queue Guard, PR-body guards, Lint and Repo Gates with every Lint shard, the Type Check shards and TypeScript Type Check, Build Core, Test Core and its shards, Test UI, Dogfood Regression Gate, Live E2E UI.

State of main since the merge: skills/objectstack-data/rules/relationships.md is byte-identical to the head (blob 52199226 on both). skills/objectstack-data/evals/objects-fields-relationships.json was changed once, by PR #33394 (6e23180), which appended eval 18 after this PR's eval 17 and touched nothing else in the file; ids on main run 1 to 18, so the id collision the PR body warned of did not happen. The changeset file is still present on main, unchanged.

① Derived judgments

  1. Accept-set: none. No schema, .describe(), export or runtime file is in the diff; ObjectSchema on main already parses sharing.controlledByParent.writeRequiresMasterRead (PR feat(spec, plugin-security): a controlled_by_parent detail can let read access on its master suffice for insert, update and delete (writeRequiresMasterRead) #33369). Right.
  2. Public surface: the published @objectstack/skills package changes prose in one rule file and gains one eval fixture entry. The heading anchor #owner-edits-under-controlled_by_parent--ownermayedit is gone; a grep of skills, docs and content on the head finds no link to it. Right.
  3. Each taught sentence, against main:
    • "by default, every write needs edit on the master": a hop whose detail does not declare the key runs assertMasterRowEditable. Right. No "byte for byte" claim (card N1).
    • "An opt-in on the detail asks read on it instead": the owner leg probes readRowById(rel.master, masterId, context); the new key swaps the hop to assertMasterRowReadable. Right.
    • ownerMayEdit: true widens only the row owner's by-id update; insert, delete, predicate updates and on-behalf-of writes keep master edit: the owner leg's conditions are operation === 'update', a stored detailRow, no opCtx.context.onBehalfOf.userId, owner_id equal to the caller. Right.
    • writeRequiresMasterRead: true, whoever reads the master inserts, updates and deletes under the detail's own object permissions, RLS and FLS: the swap is the one branch if (hopRel.writeRequiresMasterRead) in the chain walk, and single insert, batch insert, by-id and predicate writes all reach their masters through that walk; gate ① and step 2.7 run before step 2.8 either way. Right.
    • "re-points": askBodyNamedMaster calls the same walk with the detail's own relation. Right.
    • "each opted-in chain hop too": the swap is read off the hop's own relation, and the walk ends at the first read hop. Right.
    • "on-behalf-of, both users must read the master": checkControlledByParentWrite and step 2.8 run the gate once per principal, each under its own context. Right.
    • "The ownership floor (rows you created) still binds predicate and on-behalf-of updates and deletes" (card N3): the floor is created_by == current_user.id on update and delete; it is handed to the master gate only where masterGateCoversThisWrite is set, which step 2.7 sets to !delegatorSets on a by-id write and the bulk path never sets. Right.
    • "Refused beside ownerMayEdit: true, which it covers": the parse issue is raised at path sharing.controlledByParent.ownerMayEdit with "Delete ownerMayEdit; writeRequiresMasterRead already admits the owner". Right.
    • "Each is refused at parse under any other sharingModel and one level up (sharing.KEY)": both off-detail refusals exist, and the strict sharing block carries guidance for both keys one level up. Right.
    • "The detail's own row policies AND in under both": step 2.7 composes them before step 2.8 on either opt-in. Right.
    • Lint security-controlled-by-parent-owner-policy-without-opt-in is silent: if (detail.masterReadSuffices) continue; precedes both branches of the arm. Right. The same continue also silences the arm's second branch (a caller-keyed policy off owner_id); the skill does not say so, and says nothing false.
    • Optional master, "read under writeRequiresMasterRead": emptying walks the stored master, and setting one on a row with none goes nullMasterRowWritable, then askBodyNamedMaster([]), then the walk, so both ask read under the key and edit under ownerMayEdit (the owner leg steps aside for a body that empties an optional master, and a row with no master never reaches the owner leg). Right in substance; the sentence's grammar is finding N1.
  4. Eval 17: the JSON parses, ids run 1 to 17 on the head; every must_contain literal occurs in expected_output and the must_not_contain literal does not. The expected output agrees with item 3. Eval 2 (ownerMayEdit, the owner with read on the invoice) is not contradicted.
  5. The 40 deleted lines: the old section's every rule is carried into the new one. The three deleted objects (account, project, employee) were name-only ObjectSchema.create blocks in plain typescript fences, referenced by nothing else in the file; the replacing comments name them. No taught rule was lost.

② Semver level

.changeset/33374-skills-teach-write-requires-master-read.md declares '@objectstack/skills': patch. The diff publishes skill prose and one eval fixture entry and nothing else, so patch matches.

Clause-②: no, with Metadata-protocol: none, on the PR body and in the changeset. Correct: no accept-set, export or runtime change. The Check Changeset run is green.

③ Boundary flags

  • open_questions: none filed.
  • Dev deviation (1), the dev thread ran at the default model on a mandatory contract-review surface: answered by this at-tier record.
  • Dev deviation (2), the signature as a prose line: a PR-body shape matter; the PR-body guards run is green. Not a contract question.
  • Dev deviation (3), eval id 17 colliding with PR docs(skills): parent-path record-currency amounts admit no grouping; data eval for the parent-path currencyField #33394: resolved on main; docs(skills): parent-path record-currency amounts admit no grouping; data eval for the parent-path currencyField #33394 landed second as eval 18.
  • Dev out-of-scope note, the refusal shapes for a non-reader (403 on insert and re-point, 404 on by-id update and delete) and the lifecycle verbs are not taught: accepted. The skill teaches authoring; those shapes live in the ADR-0055 amendment and the permissions docs.
  • Card N1 and N3: both hold on the head (① item 3).
  • Earlier off-tier record 6883930925: it raised no must-fix and no should-fix, so there is no fix to verify on this head. Its three nits are confirmed here, independently: its N1 (the optional-master parenthetical) is N1 below; its N2 ("rows you created" on a delegated write) is N2 below; its N3 (eval 17's narrow must_not_contain) is N3 below, with one addition. None of its findings is rejected. Its ① judgments were re-derived from the tree and agree.
  • Tier H: the governed skills/** surface; the PR was merged by the maintainer path. Not this record's to judge.

④ Design judgment

skills/objectstack-data/rules/relationships.md holds against the spec, the gate and the lint on main, and against the card's TO text. Attempts to break it:

  • A reader copies the refused pair: the example carries the new key alone with the comment "or ownerMayEdit", and the bullet says the pair is refused. Holds.
  • A reader puts the key on a non-detail or one level up: both refusals are stated for both keys. Holds.
  • Over-claim of the widening on predicate or delegated writes: bounded by the floor sentence, which matches step 2.7 and the bulk path. Holds.
  • A non-declaring detail under a declaring master: the default sentence still describes what that detail's hop asks, and "each opted-in chain hop too" covers the master's own hop. Holds.
  • The table cell "read-on-master opt-ins (below)": both opt-ins ask read on the master in their scope. Holds.

skills/objectstack-data/evals/objects-fields-relationships.json holds. Eval 17's prompt is the ruled scenario (read on the order suffices, the line's own permissions decide), the expected output names the key at the right depth, the default, the two wrong answers and the floor, and the assertions are consistent with it.

Findings:

  • N1, nit. "Setting a master asks edit on it, emptying one edit on the stored master (even under ownerMayEdit; read under writeRequiresMasterRead)". The parenthetical is true of both clauses, but its position lets it be read as qualifying only the emptying clause, which would leave "setting a master asks edit" standing under the new key, where the gate asks read. The rule section above already says re-points ask read, and nothing an author declares depends on the reading. Follow-up wording only.
  • N2, nit. "The ownership floor (rows you created)" on an on-behalf-of write: both principals' floors apply, and "you" names one. True as far as it goes.
  • N3, nit. Eval 17's must_not_contain holds one spelling of the misplacement (sharing: { writeRequiresMasterRead). It does not catch the refused pair or the key placed on the master_detail field, and it can fail a correct answer that quotes the misplaced form in order to warn against it. Assertions only.

No must-fix, no should-fix. The head is merged, so each nit is a follow-up card at most, not a fix on this PR.

Implemented-by: claude/project-thread-jqd53n
Reviewed-by: session_01FEkQ5pjSPT7ui2EUcunaVb

VERDICT: PASS

Adopted by the seat 2026-11-15T00:50Z · isolated sub-agent at CONTRACT_REVIEW_TIER (served model verified by the seat) · Ruling-ref: Leehom card cmsg_01TenzsRopZj4jMbvjX8uz1XFhuiZmTH8VFgMmHRmpiGAb, option A, tapped 2026-11-14T15:37Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/s tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills: teach sharing.controlledByParent.writeRequiresMasterRead in the controlled_by_parent authoring guidance

3 participants